// Package serverutils provides protocol-agnostic building blocks for
// constructing bouncer servers. These utilities are notably used to
// implement the bouncer HTTP server that bounce.Dial can talk to.
package utils

import (
	"net"
	"time"
)

type activator func(bool) (net.Conn, error)

type hopeful struct {
	key      string
	activate activator
	created  time.Time
	err      chan error
}

// A Matchmaker is a facilitator for connecting clients wishing to
// communicate. It is essentially a helpful synchronization wrapper
// around Proxy.
type Matchmaker struct {
	// Log entries for completed proxy sessions. The user of
	// Matchmaker is expected to drain this channel, or the Matchmaker
	// will eventually deadlock.
	Log <-chan *LogEntry

	// Clients which do not find a peer within this time will be
	// disconnected with a timeout error.
	RendezvousTimeout time.Duration
	// Proxy sessions which have no network activity for this long
	// will be disconnected with a timeout error.
	ProxyTimeout time.Duration

	// In/out communication
	in  chan hopeful
	log chan *LogEntry

	// Goroutine state
	pending map[string]hopeful
	die     chan bool
	dead    chan bool
	reaper  *time.Ticker
}

const reapInterval = 2e9

func NewMatchmaker() *Matchmaker {
	rdv := &Matchmaker{
		RendezvousTimeout: 5e9,
		ProxyTimeout:      5e9,
		in:                make(chan hopeful, 10),
		log:               make(chan *LogEntry, 10),
		pending:           make(map[string]hopeful),
		die:               make(chan bool),
		dead:              make(chan bool),
		reaper:            time.NewTicker(reapInterval),
	}
	rdv.Log = rdv.log
	go rdv.run()
	return rdv
}

// Attempt to make contact with another like-minded client. If two
// clients call Add() with the same key, both their activation
// functions are run and the resulting net.Conn pair is proxied.
//
// The boolean passed to the activation functions is a client/server
// role hint like the one returned by bouncer.Dial. Activator
// implementations should relay this hint to the client, but don't
// have to.
//
// The activation function runs either to completion before Add
// returns, or not at all. If Add returns an error, the proxying
// attempt failed unrecoverably, and the failure should be relayed to
// the client.
func (r *Matchmaker) Add(key string, activate func(bool) (net.Conn, error)) error {
	err := make(chan error)
	r.in <- hopeful{key, activate, time.Now(), err}
	return <-err
}

// Shut down the helper, terminating all unmatched clients. Active
// proxy sessions are not interrupted.
func (r *Matchmaker) Close() {
	close(r.die)
	<-r.dead
	return
}

func (r *Matchmaker) run() {
	defer close(r.dead)
	for {
		select {
		case <-r.die:
			for _, v := range r.pending {
				v.err <- timeout{}
			}
			close(r.log)
			r.reaper.Stop()
			return

		case now := <-r.reaper.C:
			for k, v := range r.pending {
				if now.Sub(v.created) > r.RendezvousTimeout {
					delete(r.pending, k)
					v.err <- timeout{}
				}
			}

		case hopeful := <-r.in:
			if other, ok := r.pending[hopeful.key]; ok {
				delete(r.pending, hopeful.key)
				go r.activate(hopeful, other)
			} else {
				r.pending[hopeful.key] = hopeful
			}
		}
	}
}

func (r *Matchmaker) activate(first, second hopeful) {
	firstConn, err := first.activate(true)
	if err != nil {
		first.err <- err
		// second doesn't know what's up, so return him to the mixer.
		r.in <- second
		return
	}
	close(first.err)

	secondConn, err := second.activate(false)
	if err != nil {
		// Both connections are lost. Oh well.
		firstConn.Close()
		second.err <- err
		return
	}
	close(second.err)

	// Both connections are running, activation successful. Let the
	// two originators know and start proxying.
	r.log <- Proxy(firstConn, secondConn, r.RendezvousTimeout)
}

type timeout struct{}

func (t timeout) Error() string {
	return "Matchmaker timeout"
}

func (t timeout) Timeout() bool {
	return true
}

func (t timeout) Temporary() bool {
	return false
}
